package agngui.ui;

import agngui.event.AGNPublisher;
import agngui.event.FieldChangedEvent;
import agngui.tools.*;

import java.awt.Color;
import java.awt.event.*;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import javax.swing.*;

/**
 * This class represents a field object in its entirety. It allows for the basic
 * field information, as well as allows for an upper bound, step size, and unit
 * declaration.
 * 
 * @author Ben Bloom
 */
public class Field {

	/**
	 * string constants used for the unit Combo Boxes
	 */
	public static final String[][] unit_strings = {
			{ "Distance", "m", "a0", "nm", "cm", "km", "AU", "ly", "pc", "kpc",
					"Mpc" }, { "Angle", "deg", "rad", "arcsec" },
			{ "Mass", "kg", "amu", "g", "SM" },
			{ "Time", "s", "ms", "min", "hour", "day", "year" },
			{ "Velocity", "c", "m/s" }, { "Number Density", "m^-3", "cm^-3" }, };

	/**
	 * color used to signify that a field has changed, but has not been verified
	 */
	private static Color LIGHT_RED = new Color(255, 102, 102);
	
	/**
	 * gives back the UNIT_TYPE that the given string represents
	 * 
	 * @param units The actual units used
	 * @return the UNIT_TYPE that is the type of the parameter units
	 */
	public static UNIT_TYPE getType(String units) {
		UNIT_TYPE type = UNIT_TYPE.NONE;
		for (int k = 0; k < unit_strings.length; k++) {
			for (int i = 0; i < unit_strings[k].length; i++) {
				if (units.equals(unit_strings[k][i])) {
					switch (k) {
					case 0:
						type = UNIT_TYPE.DISTANCE;
						break;
					case 1:
						type = UNIT_TYPE.ANGLE;
						break;
					case 2:
						type = UNIT_TYPE.MASS;
						break;
					case 3:
						type = UNIT_TYPE.TIME;
						break;
					case 4:
						type = UNIT_TYPE.VELOCITY;
						break;
					case 5:
						type = UNIT_TYPE.DENSITY;
						break;
					}
					// we've found the type, break out entirely.
					k = unit_strings.length;
					break;
				}
			}
		}

		return type;
	}

	/**
	 * Given a unit type, return what the default unit is for that type for
	 * DISTANCE, the default unit is "m" for example.
	 * 
	 * @param type The UNIT_TYPE that denotes which default value is desired
	 * @return the String that is the default type for the given UNIT_TYPE
	 */
	public static String getDefaultUnit(UNIT_TYPE type) {
		String default_unit = null;
		switch (type) {
		case DISTANCE:
			default_unit = unit_strings[0][1];
			break;
		case ANGLE:
			default_unit = unit_strings[1][1];
			break;
		case MASS:
			default_unit = unit_strings[2][1];
			break;
		case TIME:
			default_unit = unit_strings[3][1];
			break;
		case VELOCITY:
			default_unit = unit_strings[4][1];
			break;
		case DENSITY:
			default_unit = unit_strings[5][1];
			break;
		}
		return default_unit;
	}

	/**
	 * the decimal formatting tool used if a number is smaller than 1e8
	 */
	public static DecimalFormat small_df = new DecimalFormat("0.#########");
	
	/**
	 * the decimal formatting tool used if a number is larger than 1e8
	 */
	public static DecimalFormat large_df = new DecimalFormat("0.#####E0");

	/**
	 * the name of this field
	 */
	private String name;
	
	/**
	 * the internal state of the field
	 */
	private FieldState myState;
	
	/**
	 * the panel which holds this field
	 */
	private RegionConfigPanel parent_panel;
	
	/**
	 * the text that was in the value field
	 */
	private String old_value_text;

	/**
	 * a handle to the value field itself
	 */
	private JTextField value_field;
	
	/**
	 * a handle to the upper bound field
	 */
	private JTextField upper_field;
	
	/**
	 * a handle to the step size field
	 */
	private JTextField step_field;
	
	/**
	 * The combo box denoting the units in use
	 */
	private JComboBox unit_box;

	/**
	 * A small wrapper which holds the state of the field
	 * 
	 * @author Ben Bloom
	 */
	private static class FieldState {
		/**
		 * the value of the state
		 */
		public String value;
		
		/**
		 * signifies if the state has an upper bound (and step size) or not
		 */
		public boolean has_upper_bound;
		
		/**
		 * the upper bound of the field
		 */
		public double upper;
		
		/**
		 * the step size of the field
		 */
		public double step;
		
		/**
		 * the unit type of this field
		 */
		public UNIT_TYPE unit_type;
		
		/**
		 * the string denoting which units are in use
		 */
		public String unit;

		/**
		 * Creates a new field state
		 * 
		 * @param v The value
		 * @param bound True if upper bounded, false if no upper bound
		 * @param up The upper bound
		 * @param s The step size
		 * @param ut The unit type
		 * @param unit The unit string
		 */
		public FieldState(String v, boolean bound, double up, double s, UNIT_TYPE ut, String unit) {
			this.value = v;
			this.has_upper_bound = bound;
			this.upper = up;
			this.step = s;
			this.unit_type = ut;
			this.unit = unit;
		}
		
		/**
		 * gives back a copy of the state
		 * 
		 * @return a deep copy of the state, copying all information and returning as a new FieldState
		 */
		public FieldState getCopy() {
			String new_unit = null;
			if(unit != null) {
				new_unit = new String(unit);
			}
			return new FieldState(new String(value), has_upper_bound, upper, step, unit_type, new_unit);
		}
		
		/**
		 * gives a string representation for this field state
		 * 
		 * @return a string representation of this field state object
		 */
		public String toString() {
			StringBuilder buf = new StringBuilder("");
			
			buf.append(value + " " + has_upper_bound + " " + upper + " " + step + " " + unit_type + " " + unit);
			
			return buf.toString();
		}
	}

	/**
	 * the stack holding the values that have been used in this field, for the purposes
	 * of using an Undo propagation.
	 */
	private LinkedList<FieldState> recent_values;

	/**
	 * constructs a new Field object
	 * 
	 * @param name The name of this field
	 * @param upper_bound true if this field has an upper bound (and step size), false if not
	 * @param units The unit type for this field
	 * @param value The actual value that goes into the base field
	 * @param upper The actual value that goes into the upper bound field
	 * @param step The actual value that goes into the step size field
	 * @param unit The actual unit used for this field
	 */
	public Field(String name, RegionConfigPanel parent_panel, boolean upper_bound, UNIT_TYPE units, String value, String upper,
			String step, String unit) {
		this.name = name;
		double up = 0;
		if(upper != null && !upper.equals("")) {
			up = Double.parseDouble(upper);
		}
		double st = 0;
		if(upper != null && !step.equals("")) {
			st = Double.parseDouble(step);
		}
		myState = new FieldState(value, upper_bound, up, st, units, unit);
		this.parent_panel = parent_panel;
		value_field = new JTextField(10);
		upper_field = new JTextField(10);
		step_field = new JTextField(10);
		value_field.setText(value);
		upper_field.setText(upper);
		step_field.setText(step);
		this.recent_values = new LinkedList<FieldState>();
		this.old_value_text = new String(value);
	}

	/**
	 * given that the field has already been constructed, and it should take on
	 * new values this function gives the new values to populate the fields.
	 *
	 * @param upper_bound true if this field has an upper bound (and step size), false if not
	 * @param units The unit type for this field
	 * @param value The actual value that goes into the base field
	 * @param upper The actual value that goes into the upper bound field
	 * @param step The actual value that goes into the step size field
	 * @param unit The actual unit used for this field
	 */
	public void repopulate(boolean upper_bound, UNIT_TYPE units, String value, String upper, String step, String unit) {

		myState.value = value;
		myState.has_upper_bound = upper_bound;
		myState.unit_type = units;
		myState.unit = unit;
		value_field.setText(value);
		if (upper_bound) {
			if(upper != null && !upper.equals("")) {
				myState.upper = Double.parseDouble(upper);
			}
			if(upper != null && !step.equals("")) {
				myState.step = Double.parseDouble(step);
			}
			upper_field.setText(upper);
			step_field.setText(step);
		} else {
			myState.upper = 0;
			myState.step = 0;
		}
		double v = 0;
		if(!myState.value.equals("")) {
			v = Double.parseDouble(myState.value);
		}
		if (units != UNIT_TYPE.NONE) {
			unit_box.setSelectedItem(unit);
			v = Converter.convertUnits(v, myState.unit, Field.getDefaultUnit(myState.unit_type));
		}
		recent_values.clear();
		push_state();
		this.old_value_text = new String(value);
		AGNPublisher.dispatchEvent(new FieldChangedEvent(this, v, false));
	}

	/**
	 * Convert's this Field's values from its current units to its default
	 * units. so something given in "pc" (parsecs) would convert to "m" (meters)
	 * for example.
	 * 
	 * @throws IllegalArgumentException if the unit types do not match
	 */
	public void convertUnits() throws IllegalArgumentException {
		if (myState.unit_type != UNIT_TYPE.NONE) {
			String default_unit = Field.getDefaultUnit(myState.unit_type);
			convertUnits(default_unit);
		}
	}

	/**
	 * Converts this Field's values from its current units to the new unit
	 * shown.
	 * 
	 * @param to_unit String denoting the units used ("m" or "s" or "m/s" for example).
	 * 
	 * @throws IllegalArgumentException if an exception occurs, such as invalid input, or missing input.
	 */
	public void convertUnits(String to_unit) throws IllegalArgumentException {
		UNIT_TYPE to_type = Field.getType(to_unit);
		if (myState.unit_type != UNIT_TYPE.NONE && myState.unit_type == to_type
				&& !myState.unit.equals(to_unit)) {
			try {
				double value = Double.parseDouble(value_field.getText());
				double converted_value = Converter.convertUnits(value, myState.unit, to_unit);
				value_field.setText(Double.toString(converted_value));
				if (myState.has_upper_bound) {
					String upper_text = upper_field.getText();
					String step_text = step_field.getText();
					if (!upper_text.equals("") && !step_text.equals("")) {
						double upper = Double.parseDouble(upper_text);
						double converted_upper = Converter.convertUnits(upper, myState.unit, to_unit);
						upper_field.setText(Double.toString(converted_upper));

						double step = Double.parseDouble(step_text);
						double converted_step = Converter.convertUnits(step, myState.unit, to_unit);
						step_field.setText(Double.toString(converted_step));
					}
				}
				unit_box.setSelectedItem(to_unit);
				myState.unit = to_unit;
			} catch (Exception e) {
				throw new IllegalArgumentException(e.getMessage());
			}
		}
	}

	/**
	 * generates the JComponents representing this field object
	 * 
	 * @return an array of JComponents to be added to a Container
	 */
	public JComponent[] generateComponents() {
		JComponent[] components = new JComponent[7];
		JLabel label = new JLabel(name);

		label.setLabelFor(value_field);

		value_field.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent e) {
				if (e.getKeyCode() == KeyEvent.VK_ENTER) {
					try {
						String value_text = value_field.getText();
						
						if (value_text.indexOf(";") == -1) {
							String problems = parent_panel.checkConstraints();
							if (problems.equals("")) {
								setValue(value_text);
								old_value_text = value_text;
								value_field.setBackground(Color.WHITE);
							} else {
								throw new NumberFormatException(problems);
							}
						} else {
							value_field.setBackground(Color.WHITE);
						}
					} catch (NumberFormatException nfe) {
						parent_panel.error("Invalid input: " + ScatteringGUI.endl + nfe.getMessage());
					}
				}
			}
		});
		
		value_field.addFocusListener(new FocusAdapter() {
			public void focusLost(FocusEvent e) {
				if(!value_field.getText().equals(old_value_text)) {
					value_field.setBackground(LIGHT_RED);
				} else {
					value_field.setBackground(Color.WHITE);
				}
			}
		});

		components[0] = label;
		components[1] = value_field;

		if (myState.has_upper_bound) {
			label = new JLabel("Upper Bound: ");
			label.setLabelFor(upper_field);
			components[2] = label;
			components[3] = upper_field;

			label = new JLabel("Step Size: ");
			label.setLabelFor(step_field);
			components[4] = label;
			components[5] = step_field;
		} else {
			for (int i = 2; i < 6; i++) {
				components[i] = new JLabel("");
			}
		}

		if (myState.unit_type != null && myState.unit_type != UNIT_TYPE.NONE) {
			unit_box = null;
			String[] strings_to_use = null;
			if (myState.unit_type == UNIT_TYPE.DISTANCE) {
				strings_to_use = unit_strings[0];
			} else if (myState.unit_type == UNIT_TYPE.ANGLE) {
				strings_to_use = unit_strings[1];
			} else if (myState.unit_type == UNIT_TYPE.MASS) {
				strings_to_use = unit_strings[2];
			} else if (myState.unit_type == UNIT_TYPE.TIME) {
				strings_to_use = unit_strings[3];
			} else if (myState.unit_type == UNIT_TYPE.VELOCITY) {
				strings_to_use = unit_strings[4];
			} else if (myState.unit_type == UNIT_TYPE.DENSITY) {
				strings_to_use = unit_strings[5];
			}
			List<String> smash = new ArrayList<String>(strings_to_use.length);
			for(int i = 1; i < strings_to_use.length; i++) {
				smash.add(strings_to_use[i]);
			}
			
			unit_box = new JComboBox(smash.toArray(new String[0]));
			if (!myState.unit.equals("")) {
				unit_box.setSelectedItem(myState.unit);
			}
			components[6] = unit_box;
			unit_box.addItemListener(new ItemListener() {
				public void itemStateChanged(ItemEvent e) {
					myState.unit = unit_box.getSelectedItem().toString();
				}
			});
		} else {
			components[6] = new JLabel("");
		}

		return components;
	}

	/**
	 * verifies that all fields are valid numbers
	 * 
	 * @throws IllegalArgumentException if a part of this field contains an invalid number
	 */
	public void verifyNumbers() throws IllegalArgumentException {
		String value = value_field.getText();
		if (!value.equals("")) {
			try {
				Double.parseDouble(value);
			} catch (NumberFormatException nfe) {
				throw new IllegalArgumentException(name + "  \"" + value + "\" is not a valid number");
			}
		}
		if (myState.has_upper_bound) {
			String upper = upper_field.getText();
			String step = step_field.getText();
			if (upper.equals("") && !step.equals("")) {
				throw new IllegalArgumentException(name + " Upper Bound is blank, but Step Size is not");
			} else if (!upper.equals("") && step.equals("")) {
				throw new IllegalArgumentException(name + " Step Size is blank, but Upper Bound is not");
			} else if (!upper.equals("") && !step.equals("")) {
				try {
					Double.parseDouble(upper);
				} catch (NumberFormatException nfe) {
					throw new IllegalArgumentException(name + " Upper Bound \"" + upper + "\" is not a number");
				}

				try {
					Double.parseDouble(step);
				} catch (NumberFormatException nfe) {
					throw new IllegalArgumentException(name + " Step Size \"" + step + "\" is not a number");
				}
			}
		}
	}

	/**
	 * verifies if all values in the value portion of the field are numbers
	 * 
	 * @return an array of all values if they're good
	 * @throws IllegalArgumentException if any value in the field is bad
	 */
	public double[] verifyList() throws IllegalArgumentException {
		String[] list = null;
		double[] retVal = null;
		String value = value_field.getText();
		if (!value.equals("")) {
			list = value.split(";");
			retVal = new double[list.length];
			try {
				for (int i = 0; i < list.length; i++) {
					retVal[i] = Double.parseDouble(list[i]);
				}
			} catch (NumberFormatException nfe) {
				throw new IllegalArgumentException(name + " contains an invalid number");
			}
		}
		return retVal;
	}
	
	/**
	 * pushes the current state onto the undo stack
	 */
	public void push_state() {
		FieldState old_state = myState.getCopy();
		recent_values.addFirst(old_state);
	}
	
	/**
	 * pops the top state off of the undo stack and replaces the values in the field with that state 
	 */
	public void pop_state() {
		//System.out.println("pop_state()");
		if(recent_values.size() > 0) {
			//System.out.println("Popping " + name + "...");
			myState = recent_values.removeFirst();
			if(recent_values.size() == 0) {
				push_state();
			}
			value_field.setText(myState.value);
			if(myState.has_upper_bound) {
				if(myState.upper == 0 && myState.step == 0) {
					upper_field.setText("");
					step_field.setText("");
				} else {
					String upper_string = null;
					if(myState.upper > 1e8) {
						upper_string = large_df.format(myState.upper);
					} else {
						upper_string = small_df.format(myState.upper);
					}
					upper_field.setText(upper_string);
					
					String step_string = null;
					if(myState.step > 1e8) {
						step_string = large_df.format(myState.step);
					} else {
						step_string = small_df.format(myState.step);
					}
					step_field.setText(step_string);
				}
			}
			if(myState.unit_type != UNIT_TYPE.NONE) {
				unit_box.setSelectedItem(myState.unit);
			}
			double value = Converter.convertUnits(Double.parseDouble(myState.value), myState.unit, Field.getDefaultUnit(myState.unit_type));
			//System.out.println("reverted value is: " + myState.value);
			AGNPublisher.dispatchEvent(new FieldChangedEvent(this, value, true));
		}
	}

	/**
	 * gives the name of this field
	 * 
	 * @return The name of this field
	 */
	public String getName() { return name;	}

	/**
	 * gives a handle to the panel which contains this field
	 * 
	 * @return The parent panel for this field
	 */
	public RegionConfigPanel getParentPanel() { return parent_panel; }

	/**
	 * gives the value of the field
	 * 
	 * @return the value in the field
	 */
	public String getValue() {
		if(value_field.getBackground().equals(Color.WHITE)) {
			value_field.setBackground(Color.WHITE);
		}
		return value_field.getText();
	}
	
	/**
	 * Gives the value of the field in default units
	 * 
	 * @return The value of the field in default units
	 */
	public String getValueInDefaultUnits() {
		String retVal = null;
		String value_text = value_field.getText();
		if(myState.unit_type != UNIT_TYPE.NONE) {
			double val = Double.parseDouble(value_text);
			double converted = Converter.convertUnits(val, myState.unit, Field.getDefaultUnit(myState.unit_type));
			retVal = Double.toString(converted); 
		} else {
			retVal = value_text;
		}
		return retVal;
	}

	/**
	 * sets the value of the field
	 * 
	 * @param v The new value for the field
	 */
	public void setValue(String v) {
		//System.out.println("state push");
		push_state();
		value_field.setText(v);
		myState.value = v;
		//System.out.println("value set to " + v);
		if(myState.value.indexOf(";") == -1) {
			double value = Double.parseDouble(myState.value);
			if(myState.unit_type != UNIT_TYPE.NONE) {
				Converter.convertUnits(value, myState.unit, Field.getDefaultUnit(myState.unit_type));
			}
			//System.out.println("generating FieldChangedEvent with " + value);
			AGNPublisher.dispatchEvent(new FieldChangedEvent(this, value, false));
		}
	}

	/**
	 * sets the value of the field
	 * 
	 * @param value The new value for the field
	 */
	public void setValue(double value) {
		String text = null;
		if (value > 1e8) {
			text = large_df.format(value);
		} else {
			text = small_df.format(value);
		}
		this.setValue(text);
	}

	/**
	 * gives the upper bound of the field
	 * 
	 * @return the upper bound field's text
	 */
	public String getUpper() { return upper_field.getText(); }

	/**
	 * gives the step size of the field
	 * 
	 * @return the step size field's text
	 */
	public String getStep() { return step_field.getText(); }

	/**
	 * gives back the units currently in use
	 * 
	 * @return the units currently in use from the unit box
	 */
	public String getUnits() { return unit_box.getSelectedItem().toString(); }

	/**
	 * gives back the unit type of this field
	 * 
	 * @return The unit type of this field
	 */
	public UNIT_TYPE getUnitType() { return myState.unit_type; }

	/**
	 * sets the unit string for this field.
	 * This will also set the unit type for the field
	 * 
	 * @param str The new string to use as the units
	 */
	public void setUnits(String str) {
		unit_box.setSelectedItem(str);
		myState.unit_type = Field.getType(str);
	}

	/**
	 * says if this field has an upper bound or not
	 * 
	 * @return true if the field has an upper bound, false if it does not
	 */
	public boolean hasUpper() { return myState.has_upper_bound; }

	/**
	 * says whether or not this field has a unit type
	 * 
	 * @return true if the field has units, false if it does not
	 */
	public boolean hasUnits() { return myState.unit_type != UNIT_TYPE.NONE; }

	/**
	 * grabs the focus of the keyboard/cursor in the UI
	 */
	public void grabFocus() { value_field.grabFocus(); }

	/**
	 * generates a string representation of the field
	 * @return a string representation of this field
	 */
	public String toString() {
		StringBuilder buf = new StringBuilder("");
		
		buf.append(name + " " + myState.toString() );
		return buf.toString();
	}

	/**
	 * performs an action equivalent to pressing the 'Enter' key in the value of the field
	 */
	public void pressEnter() { setValue(value_field.getText()); }
}
